"use client";
import * as React from "react";
import { PaginationState, SortingState, ColumnFiltersState, GroupingState } from "@tanstack/react-table";
import { ClientVirtualTable } from "@/components/client-table-v2/client-virtual-table";
import { TestProduct } from "@/db/schema/test-table-v2";
import { productColumns, orderColumns } from "./columns";
import { OrderWithDetails } from "./column-defs";
import {
getAllProducts,
getProductTableData,
getOrderTableData,
getProductTableDataWithGrouping,
GroupInfo,
} from "./actions";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
// ============================================================
// Reusable Loading Overlay Component
// ============================================================
function LoadingOverlay({
isLoading,
children
}: {
isLoading: boolean;
children: React.ReactNode
}) {
return (
{children}
{isLoading && (
)}
);
}
// ============================================================
// Pattern 1: Client-Side Table
// ============================================================
function ClientSideTable() {
const [data, setData] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const products = await getAllProducts();
setData(products);
} catch (error) {
console.error("Failed to fetch products:", error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
return (
Pattern 1: Client-Side
fetchMode="client"
모든 데이터를 한 번에 받아와 클라이언트에서 필터링/정렬/페이지네이션/그룹핑 처리합니다.
적합: 데이터 1000건 이하, 빠른 인터랙션 필요 시
✅ 그룹핑: 헤더 우클릭 → Group by [Column]
);
}
// ============================================================
// Pattern 2: Factory Service (Server-Side)
// ============================================================
function FactoryServiceTable() {
const [data, setData] = React.useState([]);
const [totalRows, setTotalRows] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(true);
// Table state
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
});
const [sorting, setSorting] = React.useState([]);
const [columnFilters, setColumnFilters] = React.useState([]);
const [globalFilter, setGlobalFilter] = React.useState("");
// Fetch data on state change
React.useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const result = await getProductTableData({
pagination,
sorting,
columnFilters,
globalFilter,
});
setData(result.data);
setTotalRows(result.totalRows);
} catch (error) {
console.error("Failed to fetch products:", error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [pagination, sorting, columnFilters, globalFilter]);
return (
Pattern 2: Factory Service
fetchMode="server"
createTableService
createTableService로 서버 액션을 자동 생성합니다.
적합: 단순 CRUD, 마스터 테이블 조회
⚠️ 그룹핑: 서버 모드에서는 별도 구현 필요 (Pattern 2-B 참고)
);
}
// ============================================================
// Pattern 2-B: Server-Side Grouping (Context Menu 방식)
// ============================================================
function ServerGroupingTable() {
const [grouping, setGrouping] = React.useState([]);
const [expandedGroups, setExpandedGroups] = React.useState([]);
const [groups, setGroups] = React.useState([]);
const [flatData, setFlatData] = React.useState([]);
const [isGrouped, setIsGrouped] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(true);
const [totalRows, setTotalRows] = React.useState(0);
const [sorting, setSorting] = React.useState([]);
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
});
// 데이터 페칭
React.useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const result = await getProductTableDataWithGrouping(
{ pagination, grouping, sorting },
expandedGroups
);
if ('groups' in result) {
setGroups(result.groups);
setIsGrouped(true);
setFlatData([]);
} else {
setFlatData(result.data);
setTotalRows(result.totalRows);
setIsGrouped(false);
setGroups([]);
}
} catch (error) {
console.error("Failed to fetch:", error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [pagination, grouping, sorting, expandedGroups]);
// 그룹 토글
const toggleGroup = (groupKey: string) => {
setExpandedGroups(prev =>
prev.includes(groupKey)
? prev.filter(k => k !== groupKey)
: [...prev, groupKey]
);
};
// 그룹핑 상태 변경 핸들러 (Context Menu에서 호출됨)
const handleGroupingChange = React.useCallback((updater: GroupingState | ((old: GroupingState) => GroupingState)) => {
const newGrouping = typeof updater === 'function' ? updater(grouping) : updater;
setGrouping(newGrouping);
setExpandedGroups([]); // 그룹핑 변경 시 확장 상태 초기화
}, [grouping]);
return (
Pattern 2-B: Server-Side Grouping
fetchMode="server"
GROUP BY
서버에서 GROUP BY + 집계 쿼리로 그룹 정보를 조회합니다.
✅ 그룹핑: 헤더 우클릭 → Group by [Column] (category, status, isNew만 지원)
{/* 현재 그룹핑 상태 표시 */}
{grouping.length > 0 && (
Grouped by:
{grouping.map((col) => (
{col}
))}
)}
{/* Content with Loading Overlay */}
{isGrouped ? (
// Grouped View - Custom Rendering
{groups.length === 0 ? (
No data
) : (
groups.map((group) => (
{/* Group Header */}
{/* Expanded Rows */}
{expandedGroups.includes(group.groupKey) && group.rows && (
| ID |
SKU |
Name |
Price |
Stock |
{group.rows.map((row) => (
| {row.id} |
{row.sku} |
{row.name} |
{new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(parseFloat(row.price))}
|
{row.stock} |
))}
)}
))
)}
) : (
// Normal Table View with Context Menu Grouping
)}
);
}
// ============================================================
// Pattern 3: Custom Service (Server-Side with Joins)
// ============================================================
function CustomServiceTable() {
const [data, setData] = React.useState([]);
const [totalRows, setTotalRows] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(true);
// Table state
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
});
const [sorting, setSorting] = React.useState([]);
const [columnFilters, setColumnFilters] = React.useState([]);
const [globalFilter, setGlobalFilter] = React.useState("");
// Fetch data on state change
React.useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const result = await getOrderTableData({
pagination,
sorting,
columnFilters,
globalFilter,
});
setData(result.data);
setTotalRows(result.totalRows);
} catch (error) {
console.error("Failed to fetch orders:", error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [pagination, sorting, columnFilters, globalFilter]);
return (
Pattern 3: Custom Service
fetchMode="server"
DrizzleTableAdapter
DrizzleTableAdapter를 도구로 사용하여 복잡한 조인 쿼리를 직접 작성합니다.
적합: 여러 테이블 조인, 복잡한 비즈니스 로직
⚠️ 그룹핑: 가상 컬럼(조인 결과)은 서버 GROUP BY 불가
);
}
// ============================================================
// Main Page
// ============================================================
export default function TableV2TestPage() {
return (
ClientVirtualTable V2 - 데이터 페칭 패턴 테스트
GUIDE.md에 정의된 데이터 페칭 패턴과 그룹핑 처리 방법을 테스트합니다.
테스트 전 시딩이 필요합니다: npx tsx db/seeds/test-table-v2.ts
1. Client-Side
2. Factory Service
2-B. Server Grouping
3. Custom Service
{/* Summary Table */}
패턴별 그룹핑 지원 현황
| 패턴 |
그룹핑 방식 |
가상 컬럼 지원 |
비고 |
| 1. Client-Side |
TanStack Grouping
|
✓ 지원
|
메모리에서 처리, 전체 데이터 필요
|
| 2. Factory Service |
미지원
|
- |
별도 구현 필요 (2-B 참고)
|
| 2-B. Server Grouping |
DB GROUP BY
|
✗ 불가
|
serverGroupable 컬럼만 가능
|
| 3. Custom Service |
커스텀 구현
|
선택적
|
쿼리 설계에 따라 다름
|
{/* Column Groupability Info */}
컬럼별 서버 그룹핑 지원 여부
meta.serverGroupable 플래그로 DB GROUP BY 가능 여부를 표시합니다.
헤더 우클릭 시 "Group by [Column]" 메뉴가 표시됩니다.
{productColumns.map((col) => {
if (!('accessorKey' in col)) return null;
const meta = col.meta as { serverGroupable?: boolean } | undefined;
const isGroupable = meta?.serverGroupable;
return (
{col.accessorKey as string}
{isGroupable && " ✓"}
);
})}
);
}